﻿using System;
using System.Security.Cryptography;
using System.Text;
using System.IO;
using System.Collections.Generic;

namespace Hids.Classes
{
    /// <summary>
    /// This class handles checking the Integrity of files using SHA1 hashes
    /// </summary>
    public class FileIntegrity
    {
        #region Private Properties

        /// <summary>
        /// The SHA1 hash of the hash file, used to ensure that the hash file has not changed
        /// </summary>
        private static string _hashFileHash = "";

        /// <summary>
        /// The file name to put the hashes in.
        /// </summary>
        public static string HashFileName = "hashes.txt";

        #endregion

        #region Private Methods

        /// <summary>
        /// Computes the SHA1 hash of a file
        /// </summary>
        /// <param name="fileToBeHashed">The path to the file that should be hashed</param>
        /// <returns>The SHA1 hash of the passed in file.  Returns an empty string if the hash could not be computed</returns>
        private static string ComputeHash(string fileToBeHashed)
        {
            List<byte> sourceBytes = new List<byte>();

            #region Read the bytes from the file

            BinaryReader reader = null;
            try
            {
                reader = new BinaryReader(new FileStream(fileToBeHashed, FileMode.Open));
                if(reader == null)
                    throw new ApplicationException("Could not read the file " + fileToBeHashed);

                while (reader.BaseStream.Position < reader.BaseStream.Length)//while there are more bytes to read
                    sourceBytes.AddRange(reader.ReadBytes(1000));//read the file, 1 kilobyte at a time

                reader.Close();//close the file
            }
            catch (Exception except)
            {
                string message = "Could not read file " + fileToBeHashed + "\n" + except.Message;
                Console.WriteLine(message);
                Logs.WriteMessageToLog(message);
                
                try
                {
                    if(reader != null)
                        reader.Close();//close the reader in case it was still open when the exception was thrown.
                }
                catch
                {}

                return "";
            }

            #endregion

            //Compute hash based on source bytes
            byte [] tmpHash = new SHA1CryptoServiceProvider().ComputeHash(sourceBytes.ToArray());
            
            string hash = "";
            foreach (byte singleByte in tmpHash)//format the hash as a hex string
                hash += string.Format("{0:X}", singleByte);
            return hash;
        }

        /// <summary>
        /// Writes the passed in file names and their respective file hashes to the hash file
        /// </summary>
        /// <param name="filesToBeHashed">The filenames to hash and write to the hash file</param>
        private static void WriteHashFile(List<string> filesToBeHashed)
        {
            StreamWriter hashFileWriter = new StreamWriter(HashFileName);

            foreach (string fileOrDirectoryToHash in filesToBeHashed)
            {
                string hash = ComputeHash(fileOrDirectoryToHash);//get the hash of the file
                if(hash != "")//if the hash was successfully computed, add it to the file
                    hashFileWriter.Write(fileOrDirectoryToHash + "," + hash + "\n");
            }

            hashFileWriter.Close();//close the hash file
            _hashFileHash = ComputeHash(HashFileName);//update the hash of the hash file
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Compares the passed in files with the hashes we have stored for those files, and sends alerts if there are any differences.  If there are no stored hashes
        /// for a file, then the Method computes the hashes for the passed in files and stores them
        /// </summary>
        /// <param name="filesToCompare">A list of filenames to compare the hashes of the files with the stored hashes</param>
        public static void CompareHashes(List<string> filesToCompare)
        {
            #region Make sure the hash file hasn't been modified

            string currentHash = ComputeHash(HashFileName);
            if (_hashFileHash != "" && currentHash != _hashFileHash)//if the hash of the hash file exists in memory and the hash of the hash file is not the same
            {//somethings wrong, the hash file has changed
                string errorMessage = "The file containing the hashes has been altered.";
                Notifications.SendEmail(errorMessage);
                Logs.WriteMessageToLog(errorMessage);
                Console.WriteLine(errorMessage);
                WriteHashFile(filesToCompare);//rewrite the hash file, becuase we can't trust it as it has been modified
                return;
            }

            #endregion

            #region Read in the hash file

            //Open file and obtain writen hash
            string fileContents;
            try
            {
                StreamReader infile = new StreamReader(HashFileName);//open file
                fileContents = infile.ReadToEnd();
                infile.Close();
            }
            catch (Exception except)
            {
                string errorMessage = "Could not get the hashes from " + HashFileName + "\n" + except.Message;
                Logs.WriteMessageToLog(errorMessage);
                Console.WriteLine(errorMessage);
                WriteHashFile(filesToCompare);
                return;
            }

            #endregion

            bool anyFileHasChanged = false;
            List<string> lines = new List<string>(fileContents.Split(new char [] {'\n'}, StringSplitOptions.RemoveEmptyEntries)); //split the hash file up into lines
            List<string> filesInHashFile = new List<string>();
            foreach (string line in lines)
            {
                #region Get the line parts

                string[] parts = line.Split(',');
                if (parts.Length != 2)
                {
                    string message = "The hash file is not in the correct format";
                    Console.WriteLine(message);
                    Logs.WriteMessageToLog(message);
                    WriteHashFile(filesToCompare);//the hash file is not in the correct format, so rewrite it into the correct format
                    return;
                }

                #endregion

                string fileName = parts[0];
                string hash = parts[1];

                filesInHashFile.Add(fileName);

                if (!filesToCompare.Contains(fileName))
                    anyFileHasChanged = true;
                else if (ComputeHash(fileName) != hash) //compute current hash and ensure it's the same
                {
                    string errorMessage = "File " + fileName + " has been altered.";
                    Notifications.SendEmail(errorMessage);
                    Logs.WriteMessageToLog(errorMessage);
                    Console.WriteLine(errorMessage);
                    WriteHashFile(filesToCompare);
                    anyFileHasChanged = true;
                }
            }

            #region Look for files that we should be comparing that aren't in the hash file
            
            foreach (string fileToCompare in filesToCompare)
            {
                if (!filesInHashFile.Contains(fileToCompare))
                {//if we find any files that we should be hashing but aren't in the hash file, recreate the hash file
                    anyFileHasChanged = true;
                    break;
                }
            }

            #endregion

            if (anyFileHasChanged)//if any changes have occured to the files, rewrite the hash file so we don't alert the next time it is called
                WriteHashFile(filesToCompare);
        }

        #endregion
    }
}
